package at.codebase.core.search;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import at.codebase.core.StringTools;
import at.codebase.core.search.SortOption.SortOptionDetail;
import at.codebase.core.searchUser.UsernameSearch;
import at.codebase.db.dao.EItemDao;
import at.codebase.db.entities.EItem;

import com.google.appengine.api.search.Document;
import com.google.appengine.api.search.Field;
import com.google.appengine.api.search.GeoPoint;
import com.google.appengine.api.search.GetRequest;
import com.google.appengine.api.search.GetResponse;
import com.google.appengine.api.search.Index;
import com.google.appengine.api.search.IndexSpec;
import com.google.appengine.api.search.MatchScorer;
import com.google.appengine.api.search.Query;
import com.google.appengine.api.search.QueryOptions;
import com.google.appengine.api.search.Results;
import com.google.appengine.api.search.ScoredDocument;
import com.google.appengine.api.search.SearchServiceFactory;
import com.google.appengine.api.search.SortExpression;
import com.google.appengine.api.search.SortExpression.SortDirection;
import com.google.appengine.api.search.SortOptions;
import com.google.appengine.api.search.SortOptions.Builder;

/**
 * Helper class to search for items using a specified criteria.
 * 
 * @author Daniel Marth <danielmarth@gmx.at>
 */
public class ItemSearch {

	public static void init() {
	}

	private static final Logger log = Logger.getLogger(UsernameSearch.class
			.getName());

	private static Index index = SearchServiceFactory.getSearchService()
			.getIndex(IndexSpec.newBuilder().setName("ItemIndex").build());
	private static final int SearchNumberAccuracy = 1000;

	static {
		rebuildIndex();
	}

	public static Index getIndex() {
		return index;
	}

	private static Document buildDocument(EItem item) {
		Document.Builder builder = Document
				.newBuilder()
				.setId(item.getId().toString())
				.addField(
						Field.newBuilder()
								.setName("userid")
								.setText(
										item.getSeller().get().getId()
												.toString()))
				.addField(
						Field.newBuilder().setName("title")
								.setText(item.getTitle()))
				.addField(
						Field.newBuilder()
								.setName("titleSubstrings")
								.setText(
										StringTools.generateSubstrings(item
												.getTitle())))
				.addField(
						Field.newBuilder().setName("description")
								.setText(item.getDescription()))
				.addField(
						Field.newBuilder().setName("price")
								.setText(String.valueOf(item.getPrice())))
				.addField(
						Field.newBuilder().setName("timeCreate")
								.setNumber(item.getTimeCreate() / 1000))
				.addField(
						Field.newBuilder().setName("timeExpiration")
								.setNumber(item.getTimeExpiration() / 1000));
		if (item.getLocation() != null) {
			builder.addField(Field.newBuilder().setName("location")
					.setGeoPoint(item.getLocation()));
		} else {
			builder.addField(Field.newBuilder().setName("location")
					.setGeoPoint(new GeoPoint(-90.0, 180.0)));
		}
		return builder.build();
	}

	public static void putItems(final List<EItem> items) {
		ArrayList<Document> docs = new ArrayList<Document>();
		for (EItem item : items) {
			docs.add(buildDocument(item));
			if (docs.size() % 200 == 0) {
				getIndex().put(docs);
				docs.clear();
			}
		}
		getIndex().put(docs);
	}

	public static void putItem(final EItem item) {
		getIndex().put(buildDocument(item));
	}

	public static void removeItem(final EItem item) {
		index.delete(new String[] { item.getId().toString() });
	}

	public static void removeItems(ArrayList<Long> itemIds) {
		ArrayList<String> stringIds = new ArrayList<String>();
		for (Long id : itemIds) {
			stringIds.add(id.toString());
		}
		index.delete(stringIds);
	}

	public static ItemSearchResultInfo search(final String queryStr,
			final int limit, final int offset) {
		ArrayList<ItemSearchResult> searchResults = new ArrayList<ItemSearchResult>();
		Query queryObject = Query
				.newBuilder()
				.setOptions(
						QueryOptions
								.newBuilder()
								.setNumberFoundAccuracy(SearchNumberAccuracy)
								.setLimit(limit)
								.setOffset(offset)
								.setFieldsToSnippet("description", "title")
								.setSortOptions(
										SortOptions.newBuilder()
												.setMatchScorer(
														MatchScorer
																.newBuilder())))
				.build(new ItemSearchQuery(queryStr).compile());
		Results<ScoredDocument> queryResults = index.search(queryObject);
		ArrayList<Long> itemIds = new ArrayList<Long>();
		for (ScoredDocument doc : queryResults) {
			List<Field> expressions = doc.getExpressions();
			String descriptionSnippet = "";
			String titleSnippet = "";
			if (expressions != null) {
				for (Field field : expressions) {
					if ("description".equals(field.getName())) {
						descriptionSnippet = field.getHTML();
					} else if ("title".equals(field.getName())) {
						titleSnippet = field.getHTML();
					}
				}
			}
			itemIds.add(new Long(doc.getId()));
			searchResults.add(new ItemSearchResult(titleSnippet,
					descriptionSnippet));

		}
		return new ItemSearchResultInfo(queryResults.getNumberFound(),
				searchResults, EItemDao.getItemsByIds(itemIds));
	}

	public static ItemSearchResultInfo searchSorted(final String queryStr,
			final int limit, final int offset, ArrayList<SortOption> sortItems) {
		ArrayList<ItemSearchResult> searchResults = new ArrayList<ItemSearchResult>();
		Builder sortOptionsBuilder = SortOptions.newBuilder();
		for (SortOption option : sortItems) {
			SortDirection direction;
			if (option.getDetail().equals(SortOptionDetail.ASCENDING)) {
				direction = SortDirection.ASCENDING;
			} else {
				direction = SortDirection.DESCENDING;
			}
			String type = SortOption.getStringType(option.getType());
			sortOptionsBuilder.addSortExpression(SortExpression.newBuilder()
					.setExpression(type).setDirection(direction)
					.setDefaultValue(""));
		}

		SortOptions sortOptions = sortOptionsBuilder.build();
		Query queryObject = Query
				.newBuilder()
				.setOptions(
						QueryOptions.newBuilder()
								.setNumberFoundAccuracy(SearchNumberAccuracy)
								.setLimit(limit).setOffset(offset)
								.setFieldsToSnippet("description", "title")
								.setSortOptions(sortOptions).build())
				.build(new ItemSearchQuery(queryStr).compile());
		Results<ScoredDocument> queryResults = index.search(queryObject);
		ArrayList<Long> itemIds = new ArrayList<Long>();
		for (ScoredDocument doc : queryResults) {
			List<Field> expressions = doc.getExpressions();
			String descriptionSnippet = "";
			String titleSnippet = "";
			if (expressions != null) {
				for (Field field : expressions) {
					if ("description".equals(field.getName())) {
						descriptionSnippet = field.getHTML();
					} else if ("title".equals(field.getName())) {
						titleSnippet = field.getHTML();
					}
				}
			}
			itemIds.add(new Long(doc.getId()));
			searchResults.add(new ItemSearchResult(titleSnippet,
					descriptionSnippet));
		}
		return new ItemSearchResultInfo(queryResults.getNumberFound(),
				searchResults, EItemDao.getItemsByIds(itemIds));
	}

	/**
	 * 
	 * @param queryStr
	 * @param limit
	 * @param offset
	 * @param location
	 * @param range
	 *            Distance from location in meters.
	 * @return
	 */
	public static ItemSearchResultInfo searchClosest(final String queryStr,
			final int limit, final int offset, ArrayList<SortOption> sortItems,
			GeoPoint location, final int range) {
		return searchSorted(queryStr, limit, offset, sortItems);
		/*
		 * ArrayList<ItemSearchResult> searchResults = new
		 * ArrayList<ItemSearchResult>(); Builder sortOptionsBuilder =
		 * SortOptions.newBuilder(); SecureStringBuilder queryBuilder = new
		 * SecureStringBuilder(); for (SortOption option : sortItems) {
		 * SortDirection direction; if
		 * (option.getDetail().equals(SortOptionDetail.ASCENDING)) { direction =
		 * SortDirection.ASCENDING; } else { direction =
		 * SortDirection.DESCENDING; } if (option.getType() ==
		 * SortOptionType.LOCATION) { SecureStringBuilder builder = new
		 * SecureStringBuilder();
		 * builder.appendInsecure("distance(location, geopoint(");
		 * builder.appendInsecure(String.valueOf(location.getLatitude()));
		 * builder.appendInsecure(", ");
		 * builder.appendInsecure(String.valueOf(location.getLongitude()));
		 * builder.appendInsecure("))"); String locationQuery =
		 * builder.toString(); builder.appendInsecure(" <= ");
		 * builder.appendInsecure(String.valueOf(range)); String
		 * locationExpression = builder.toString();
		 * queryBuilder.appendInsecure("("); queryBuilder.appendInsecure(new
		 * ItemSearchQuery(queryStr) .compile(/* customQueryFieldNames *FIXME
		 * comment)); queryBuilder.appendInsecure(") AND ");
		 * queryBuilder.appendInsecure(locationQuery);
		 * sortOptionsBuilder.addSortExpression(SortExpression
		 * .newBuilder().setExpression(locationExpression)
		 * .setDirection(direction) .setDefaultValueNumeric(range + 1)); } else
		 * { String type = SortOption.getStringType(option.getType());
		 * sortOptionsBuilder.addSortExpression(SortExpression
		 * .newBuilder().setExpression(type)
		 * .setDirection(direction).setDefaultValue("")); } }
		 * 
		 * Query queryObject = Query .newBuilder() .setOptions(
		 * QueryOptions.newBuilder()
		 * .setNumberFoundAccuracy(SearchNumberAccuracy)
		 * .setLimit(limit).setOffset(offset) .setFieldsToSnippet("description",
		 * "title") .setSortOptions(sortOptionsBuilder.build())
		 * .build()).build(queryBuilder.toString()); Results<ScoredDocument>
		 * queryResults = index.search(queryObject); ArrayList<Long> itemIds =
		 * new ArrayList<Long>(); for (ScoredDocument doc : queryResults) {
		 * List<Field> expressions = doc.getExpressions(); String
		 * descriptionSnippet = ""; String titleSnippet = ""; if (expressions !=
		 * null) { for (Field field : expressions) { if
		 * ("description".equals(field.getName())) { descriptionSnippet =
		 * field.getHTML(); } else if ("title".equals(field.getName())) {
		 * titleSnippet = field.getHTML(); } } } Iterable<Field> fields =
		 * doc.getFields(); String timeExpiration = null; if (fields != null) {
		 * for (Field field : fields) { if
		 * ("timeExpiration".equals(field.getName())) { timeExpiration =
		 * field.getText(); } } } if (timeExpiration != null &&
		 * !((System.currentTimeMillis()) > Long .parseLong(timeExpiration))) {
		 * itemIds.add(new Long(doc.getId())); searchResults.add(new
		 * ItemSearchResult(titleSnippet, descriptionSnippet)); } else {
		 * getIndex().delete(new String[] { doc.getId() }); } } return new
		 * ItemSearchResultInfo(queryResults.getNumberFound(), searchResults,
		 * EItemDao.getItemsByIds(itemIds));
		 */
	}

	public static void rebuildIndex() {
		long startTime = System.currentTimeMillis();
		clearIndex();
		buildIndex();
		long endTime = System.currentTimeMillis();
		log.info("Searchindex (item) - rebuild - " + (endTime - startTime)
				+ " ms");
	}

	public static void buildIndex() {
		long startTime = System.currentTimeMillis();
		List<EItem> items = EItemDao.getAllItems();
		for (EItem item : items) {
			putItem(item);
		}
		long endTime = System.currentTimeMillis();
		log.info("Searchindex (item) - build - " + (endTime - startTime)
				+ " ms");
	}

	public static void clearIndex() {
		long startTime = System.currentTimeMillis();
		while (true) {
			List<String> docIds = new ArrayList<String>();
			GetRequest request = GetRequest.newBuilder()
					.setReturningIdsOnly(true).build();
			GetResponse<Document> response = getIndex().getRange(request);
			if (response.getResults().isEmpty()) {
				break;
			}
			for (Document doc : response) {
				docIds.add(doc.getId());
			}
			getIndex().delete(docIds);
		}
		long endTime = System.currentTimeMillis();
		log.info("Searchindex (item) - clear - " + (endTime - startTime)
				+ " ms");
	}

}
